/*
 * Copyright (C) 2017 Fabian Terhorst
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file kt in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.fabianterhorst.isometric;

import io.reactivex.rxjava3.annotations.Nullable;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

/**
 * Isometric.
 *
 * @since 2021-03-11
 */
public class Isometric {
    /**
     * To get class name
     */
    private static final String TAG = Isometric.class.getName();

    private static final int MAGICNUMBER_SIX = 6;

    private static final int MAGICNUMBER_SEVENTY = 70;

    private boolean sort = true;

    private boolean cull = false;

    private boolean boundsCheck = false;

    private boolean reverseSortForLookup = false;

    private boolean touchRadiusLookup = false;

    private final double angle;

    private final double scale;

    private double[][] transformation;

    private double originX;

    private double originY;

    private List<Item> items = new ArrayList<>();

    private final Vector lightAngle;

    private final double colorDifference;

    private final Color lightColor;

    private int currentWidth;

    private int currentHeight;

    private boolean itemsChanged;

    /**
     * Default Constructor
     * Isometric.
     */
    public Isometric() {
        angle = Math.PI / MAGICNUMBER_SIX;
        scale = MAGICNUMBER_SEVENTY;
        transformation = new double[][] {
            {
                scale * Math.cos(angle), scale * Math.sin(angle)
            }, {
            scale * Math.cos(Math.PI - angle), scale * Math.sin(Math.PI - angle)
        }
        };
        Vector lightPosition = new Vector(2, -1, 3);
        lightAngle = lightPosition.normalize();
        colorDifference = 0.20;
        lightColor = new Color(255, 255, 255);
        currentWidth = -1;
        currentHeight = -1;
        itemsChanged = true;
    }

    /**
     * OriginX rides along the angle extended from the origin
     * OriginY rides perpendicular to this angle (in isometric view: PI - angle)
     * OriginZ affects the y coordinate of the drawn point
     */

    /**
     * method filter is used to find count of list items which are starts with #prefix
     *
     * @param point {@Link Point}
     * @return Point
     */
    public Point translatePoint(Point point) {
        HiLogs.d(TAG, "translatePoint : " + point);
        return new Point(originX + point.x * transformation[0][0] + point.y * transformation[1][0],
            originY - point.x * transformation[0][1] - point.y * transformation[1][1] - (point.z * scale));
    }

    /**
     * Call addPath method with original shape
     *
     * @param path {@link Path}
     * @param color {@link Color}
     */
    public void add(Path path, Color color) {
        HiLogs.d(TAG, "add Path" + path + "add Color" + color);
        addPath(path, color, null);
    }

    /**
     * Call addPath method with original shape
     *
     * @param path {@link Path}
     * @param color {@link Color}
     * @param originalShape {@link Shape}
     */
    public void add(Path path, Color color, Shape originalShape) {
        HiLogs.d(TAG, "add() path : " + path + "add() color : " + color + "add() originalShape : " + originalShape);
        addPath(path, color, originalShape);
    }

    /**
     * Call addPath method with original shape
     *
     * @param paths {@link Path}
     * @param color @link Color}
     */
    public void add(Path[] paths, Color color) {
        HiLogs.d(TAG, "add() path : " + (paths == null) + paths.length + "add() color : " + color);
        for (Path path : paths) {
            add(path, color);
        }
    }

    /**
     * Call addPath method with original shape
     *
     * @param paths {@link Path}
     * @param color @link Color}
     * @param originalShape @link Shape}
     */
    public void add(Path[] paths, Color color, Shape originalShape) {
        HiLogs.d(TAG,
            "add() path : " + (paths == null) + paths.length + "add() color : " + color + "add() originalShape : "
                + originalShape);
        for (Path path : paths) {
            add(path, color, originalShape);
        }
    }

    /**
     * Call addPath method with original shape
     *
     * @param shape {@link Shape}
     * @param color @link Color}
     */
    public void add(Shape shape, Color color) {
        Path[] paths = shape.orderedPaths();
        HiLogs.d(TAG, "add() path : " + (paths == null) + paths.length + "add() color : " + color);
        for (Path path : paths) {
            addPath(path, color, shape);
        }
    }

    /**
     * Used to clear items
     */
    public void clear() {
        HiLogs.d(TAG, "clear()");
        itemsChanged = true;
        items.clear();
    }

    /**
     * Call addPath method with original shape
     *
     * @param path {@link Path}
     * @param color @link Color}
     * @param originalShape @link Shape}
     */
    private void addPath(Path path, Color color, Shape originalShape) {
        HiLogs.d(TAG,
            "addPath() path : " + path + "addPath() color : " + color + "addPath() originalShape : " + originalShape);
        itemsChanged = true;
        items.add(new Item(path, transformColor(path, color), originalShape));
    }

    /**
     * Call addPath method with original shape
     *
     * @param path {@link Path}
     * @param color @link Color}
     * @return color
     */
    private Color transformColor(Path path, Color color) {
        HiLogs.d(TAG, "transformColor() path : " + path + "transformColor() color : " + color);
        Point p1 = path.points[1];
        Point p2 = path.points[0];
        double i = p2.x - p1.x;
        double j = p2.y - p1.y;
        double k = p2.z - p1.z;
        p1 = path.points[2];
        p2 = path.points[1];
        double i2 = p2.x - p1.x;
        double j2 = p2.y - p1.y;
        double k2 = p2.z - p1.z;
        double i3 = j * k2 - j2 * k;
        double j3 = -1 * (i * k2 - i2 * k);
        double k3 = i * j2 - i2 * j;
        double magnitude = Math.sqrt(i3 * i3 + j3 * j3 + k3 * k3);
        i = magnitude == 0 ? 0 : i3 / magnitude;
        j = magnitude == 0 ? 0 : j3 / magnitude;
        k = magnitude == 0 ? 0 : k3 / magnitude;
        double brightness = i * lightAngle.i + j * lightAngle.j + k * lightAngle.k;
        return color.lighten(brightness * colorDifference, lightColor);
    }

    /**
     * measure
     *
     * @param width {@link int}
     * @param height {@link int}
     * @param sort {@link boolean}
     * @param cull {@link boolean}
     * @param boundsCheck {@link boolean}
     */
    public void measure(int width, int height, boolean sort, boolean cull, boolean boundsCheck) {
        HiLogs.d(TAG, "measure() width : " + width + "measure() height : " + height + "measure() sort : " + sort
            + "measure() cull : " + cull + "measure() boundsCheck : " + boundsCheck);
        /** only perform measure operation:
         * if the bounds have changed
         * OR if the items have changed
         **/
        if (currentWidth == width && currentHeight == height && !itemsChanged) {
            return;
        }

        currentWidth = width;
        currentHeight = height;
        itemsChanged = false;

        originX = width / 2;
        originY = height * 0.9;

        int itemIndex = 0;
        int itemSize = items.size();
        while (itemIndex < itemSize) {
            Item item = items.get(itemIndex);

            item.transformedPoints = new Point[item.path.points.length];

            if (!item.drawPath.isEmpty()) {
                item.drawPath.rewind();
            }

            Point point;
            for (int i = 0, length = item.path.points.length; i < length; i++) {
                point = item.path.points[i];
                item.transformedPoints[i] = translatePoint(point);
            }

            /**
             *  remove item if not in view
             *  the if conditions here are ordered carefully to save computation, fail fast approach
             **/
            if ((cull && cullPath(item)) || (boundsCheck && !itemInDrawingBounds(item))) {
                /**
                 * the path is invisible. It does not need to be considered any more
                 */
                items.remove(itemIndex);
                itemSize--;
                continue;
            } else {
                itemIndex++;
            }

            item.drawPath.moveTo((float) item.transformedPoints[0].x, (float) item.transformedPoints[0].y);

            for (int i = 1, length = item.transformedPoints.length; i < length; i++) {
                item.drawPath.lineTo((float) item.transformedPoints[i].x, (float) item.transformedPoints[i].y);
            }

            item.drawPath.close();
        }

        if (sort) {
            items = sortPaths();
        }
    }

    /**
     * measure
     *
     * @param item {@link Item}
     * @return calculations after transform points
     */
    private boolean cullPath(Item item) {
        HiLogs.d(TAG, "cullPath() item : " + item);
        double a = item.transformedPoints[0].getX() * item.transformedPoints[1].getY();
        double b = item.transformedPoints[1].getX() * item.transformedPoints[2].getY();
        double c = item.transformedPoints[2].getX() * item.transformedPoints[0].getY();

        double d = item.transformedPoints[1].getX() * item.transformedPoints[0].getY();
        double e = item.transformedPoints[2].getX() * item.transformedPoints[1].getY();
        double f = item.transformedPoints[0].getX() * item.transformedPoints[2].getY();

        double z = a + b + c - d - e - f;
        return z > 0;
    }

    /**
     * itemInDrawingBounds
     *
     * @param item {@link  Item}
     * @return false
     */
    private boolean itemInDrawingBounds(Item item) {
        HiLogs.d(TAG, "itemInDrawingBounds() item : " + item);
        HiLogs.d(TAG, "itemInDrawingBounds() item.transformedPoints.length : " + item.transformedPoints.length);

        for (int i = 0, len = item.transformedPoints.length; i < len; i++) {
            /**
             * if any point is in bounds, the item is worth drawing
             **/
            if (item.transformedPoints[i].getX() >= 0 && item.transformedPoints[i].getX() <= currentWidth
                && item.transformedPoints[i].getY() >= 0 && item.transformedPoints[i].getY() <= currentHeight) {
                return true;
            }
        }
        return false;
    }

    /**
     * sortPaths
     *
     * @return sortedItems
     */
    private List<Item> sortPaths() {
        HiLogs.d(TAG, "ist<Item> sortPaths() : ");
        ArrayList<Item> sortedItems = new ArrayList<>();
        Point observer = new Point(-10, -10, 20);
        int length = items.size();
        List<List<Integer>> drawBefore = new ArrayList<>(length);
        for (int i = 0; i < length; i++) {
            drawBefore.add(i, new ArrayList<Integer>());
        }
        Item itemA;
        Item itemB;
        for (int i = 0; i < length; i++) {
            itemA = items.get(i);
            for (int j = 0; j < i; j++) {
                itemB = items.get(j);
                if (IntersectionUtils.hasIntersection(itemA.transformedPoints, itemB.transformedPoints)) {
                    int cmpPath = itemA.path.closerThan(itemB.path, observer);
                    if (cmpPath < 0) {
                        drawBefore.get(i).add(j);
                    } else if (cmpPath > 0) {
                        drawBefore.get(j).add(i);
                    }
                }
            }
        }
        int drawThisTurn = 1;
        Item currItem;
        List<Integer> integers;
        while (drawThisTurn == 1) {
            drawThisTurn = 0;
            for (int i = 0; i < length; i++) {
                currItem = items.get(i);
                integers = drawBefore.get(i);
                if (currItem.drawn == 0) {
                    int canDraw = 1;
                    for (int j = 0, lengthIntegers = integers.size(); j < lengthIntegers; j++) {
                        if (items.get(integers.get(j)).drawn == 0) {
                            canDraw = 0;
                            break;
                        }
                    }
                    if (canDraw == 1) {
                        Item item = new Item(currItem);
                        sortedItems.add(item);
                        currItem.drawn = 1;
                        items.set(i, currItem);
                        drawThisTurn = 1;
                    }
                }
            }
        }

        for (int i = 0; i < length; i++) {
            currItem = items.get(i);
            if (currItem.drawn == 0) {
                sortedItems.add(new Item(currItem));
            }
        }
        return sortedItems;
    }

    /**
     * method draw is used to draw() geometrical fig
     *
     * @param canvas {@link int}
     * @param width {@link int}
     * @param height {@link int}
     */
    public void draw(Canvas canvas, int width, int height) {
        measure(width, height, sort, cull, boundsCheck);
        HiLogs.d(TAG, "draw() : " + canvas + "draw() : " + width + "draw() : " + height);
        for (Item item : items) {
            canvas.drawPath(item.drawPath, item.paint);
        }
    }

    /**
     * method filter is used to find count of list items which are starts with #prefix
     *
     * @param position {@link Point}
     * @param reverseSort {@link boolean}
     * @param touchPosition {@link boolean}
     * @param radius {@link double}
     * @return null
     */
    @Nullable
    public Item findItemForPosition(Point position, boolean reverseSort, boolean touchPosition, double radius) {
        HiLogs.d(TAG,
            "findItemForPosition() position : " + position + "findItemForPosition() reverseSort : " + reverseSort
                + "findItemForPosition() touchPosition : " + touchPosition + "findItemForPosition() radius : "
                + radius);
        /**
         * get iterator for the items list, and start either at the front or back
         * The items are already sorted back-to-front, by iterating the items list backwards
         * you check the items closer to the user first
         */
        ListIterator<Item> itr = items.listIterator(reverseSort ? items.size() : 0);

        /**
         * Items are already sorted for depth sort so break should not be a problem here
         * terate through the list in one direction or the other
         */
        while (reverseSort ? itr.hasPrevious() : itr.hasNext()) {
            Item item = reverseSort ? itr.previous() : itr.next();

            if (item.transformedPoints == null) {
                continue;
            }
            List<Point> items = new ArrayList<>();
            Point top = null, bottom = null, left = null, right = null;
            for (Point point : item.transformedPoints) {
                if (top == null) {
                    top = new Point(point.x, point.y);
                } else if (point.y > top.y) {
                    top.y = point.y;
                    top.x = point.x;
                }

                if (bottom == null) {
                    bottom = new Point(point.x, point.y);
                } else if (point.y < bottom.y) {
                    bottom.y = point.y;
                    bottom.x = point.x;
                }

                if (left == null) {
                    left = new Point(point.x, point.y);
                } else if (point.x < left.x) {
                    left.x = point.x;
                    left.y = point.y;
                }

                if (right == null) {
                    right = new Point(point.x, point.y);
                } else if (point.x > right.x) {
                    right.x = point.x;
                    right.y = point.y;
                }
            }

            items.add(left);
            items.add(top);
            items.add(right);
            items.add(bottom);
            /**
             * search for equal points that are above or below for left and right or left and right for bottom and top
             */
            for (Point point : item.transformedPoints) {
                if (point.x == left.x) {
                    if (point.y != left.y) {
                        items.add(point);
                    }
                }
                if (point.x == right.x) {
                    if (point.y != right.y) {
                        items.add(point);
                    }
                }
                if (point.y == top.y) {
                    if (point.y != top.y) {
                        items.add(point);
                    }
                }
                if (point.y == bottom.y) {
                    if (point.y != bottom.y) {
                        items.add(point);
                    }
                }
            }
            /* perform one method of touch position lookup
             * it is faster to check the individual segments first (disabled by default).
             *its possible the touch center is inside poly, but not close to
             * an edge so finish by checking if center of circle is in poly
             * */
            if ((touchPosition && IntersectionUtils.isPointCloseToPoly(items, position.x, position.y, radius))
                || IntersectionUtils.isPointInPoly(items, position.x, position.y)) {
                return item;
            }
        }
        return null;
    }

    /**
     * Item
     *
     * @since 2021-03-11
     */
    public static class Item {
        private static final String TAG = Item.class.getName();

        Path path;

        Color baseColor;

        Paint paint;

        Shape originalShape;

        int drawn;

        Point[] transformedPoints;

        ohos.agp.render.Path drawPath;

        /**
         * @param item {@link Item}
         */
        Item(Item item) {
            HiLogs.d(TAG, "Item : " + item);
            transformedPoints = item.transformedPoints;
            drawPath = item.drawPath;
            drawn = item.drawn;
            paint = item.paint;
            path = item.path;
            baseColor = item.baseColor;
            originalShape = item.originalShape;
        }

        /**
         * @param path {@link Path}
         * @param baseColor {@link Color}
         * @param originalShape {@link Shape}
         */
        Item(Path path, Color baseColor, Shape originalShape) {
            HiLogs.d(TAG,
                "Item path : " + path + "Item baseColor : " + baseColor + "Item originalShape : " + originalShape);
            drawPath = new ohos.agp.render.Path();
            drawn = 0;
            paint = new Paint();
            paint.setAntiAlias(true);
            paint.setStyle(Paint.Style.FILLANDSTROKE_STYLE);
            paint.setStrokeWidth(1);
            this.path = path;
            this.baseColor = baseColor;
            this.originalShape = originalShape;
            paint.setColor(new ohos.agp.utils.Color(
                ohos.agp.utils.Color.argb((int) baseColor.a, (int) baseColor.r, (int) baseColor.g, (int) baseColor.b)));
        }

        /**
         * Used to get path
         *
         * @return path
         */
        public Path getPath() {
            HiLogs.d(TAG, "Path getPath() : ");
            return path;
        }

        /**
         * Used to get original shape of fig
         *
         * @return originalShape
         */
        public Shape getOriginalShape() {
            HiLogs.d(TAG, "Shape getOriginalShape() : ");
            return originalShape;
        }
    }
}
